package com.stark.commons.spring.web.interceptor;

import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 安全拦截器。
 * <p>配置参数 <code>web.security-check</code> (true/false) 控制是否安全检测。
 * <ul>
 * <li>拦截带 ".xxx" 后缀的请求；</li>
 * <li>防止 SQL 注入；</li>
 * <li>防止 XSS 攻击。</li>
 * </ul>
 * <p><strong>警告：</strong>非必要情况下不建议开启，否则会影响性能！
 * @author Ben
 * @since 1.0.0
 * @version 1.0.0
 */
@NoArgsConstructor
@Slf4j
public class SecurityInterceptor extends HandlerInterceptorAdapter {
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		if (handler.getClass().isAssignableFrom(HandlerMethod.class)) {
			String url = request.getRequestURL().toString();
			String uri = request.getRequestURI();
			String queryString = request.getQueryString();
			HandlerMethod handlerMethod = (HandlerMethod) handler;
			
			if (url.contains("swagger") || url.contains("v2/api-docs")) {
				return true;
			}
			
			// 1. 检查 uri 是否以 ".xxx" 结尾，是则抛出异常
			int dotIndex = uriEndWithDot(uri);
			if (dotIndex > -1) {
				throw new URISyntaxException(uri, "包含 \".xxx\" 后缀", dotIndex);
			}
			
			// 2. 不检查无参方法
			MethodParameter[] parameters = handlerMethod.getMethodParameters();
			if (ArrayUtils.isEmpty(parameters)) {
				return true;
			}
			
			// 3. 检查 @PathVariable 参数是否存在攻击风险
			NativeWebRequest webRequest = new ServletWebRequest(request);
			@SuppressWarnings("unchecked")
			Map<String, Object> map = (Map<String, Object>) webRequest.getAttribute(View.PATH_VARIABLES, RequestAttributes.SCOPE_REQUEST);
			if (MapUtils.isNotEmpty(map)) {
				for (Entry<String, Object> entry : map.entrySet()) {
					checkSafe(url, queryString, entry.getKey(), entry.getValue().toString());
				}
			}
			
			// 4. 检查 queryString 中的参数是否存在攻击风险
			Enumeration<String> names = request.getParameterNames();
			String uuid = UUID.randomUUID().toString();
			while (names.hasMoreElements()) {
				String key = names.nextElement();
				String value = uuid;
				try {
					value = request.getParameter(key);
				} catch (Exception e) {	// 跳过无法转换成字符型的参数
					continue;
				}
				
				checkSafe(url, queryString, key, value);
			}
		}
		
		return true;
	}
	
	/**
	 * uri 是否以 ".xxx" 结尾。
	 * @param uri
	 * @return 是返回 "." 的索引，否返回 -1 。
	 */
	private int uriEndWithDot(String uri) {
		if (StringUtils.isBlank(uri)) {
			return -1;
		}
		
		int dotIndex = uri.lastIndexOf(".");
		if (dotIndex > 0 && dotIndex < uri.length()) {
			return dotIndex;
		}
		return -1;
	}
	
	/**
	 * 检查参数是否安全。
	 * @param url 请求地址。
	 * @param queryString 查询字符串。
	 * @param key 参数键。
	 * @param value 参数值。
	 */
	private void checkSafe(String url, String queryString, String key, String value) {
		if (isSQLInject(value)) {
			String info = new StringBuffer()
					.append("请求[").append(url).append(!StringUtils.isBlank(queryString) ? "?" + queryString : "").append("]被禁止：")
					.append("参数 ").append(key).append(" 的值 ").append(value).append(" 存在 SQL 注入风险")
					.toString();
			log.error(info);
			throw new IllegalArgumentException(info);
		}
		if (isXSS(value)) {
			String info = new StringBuffer()
					.append("请求[").append(url).append(!StringUtils.isBlank(queryString) ? "?" + queryString : "").append("]被禁止：")
					.append("参数 ").append(key).append(" 的值 ").append(value).append(" 存在 XSS 攻击风险")
					.toString();
			log.error(info);
			throw new IllegalArgumentException(info);
		}
	}
	
	/**
	 * 是否 SQL 注入。
	 * @param value 字符串型的参数值。
	 * @return
	 */
	private boolean isSQLInject(String value) {
		if (StringUtils.isBlank(value)) {
			return false;
		}
		value = value.toLowerCase();
		if (value.contains("%")) {
			try {
				value = URLDecoder.decode(value, "UTF-8");
			} catch (UnsupportedEncodingException e) {
				log.error("参数值转码失败：value={}", value, e);
				return true;
			}
		}
		// 处理其他字符：|and|exec|insert|select|delete|update|count|*|%|chr|mid|master|truncate|char|declare|;|or|-|+|,
		String injectStr = "'|and |exec|insert|select|delete|update|count|*|%|chr|mid|master|truncate|char|declare|or |+";
		String strArray[] = injectStr.split("\\|");
		for (String str : strArray) {
			if (value.contains(str)) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * 是否 XSS 攻击。
	 * @param value 字符串型的参数值。
	 * @return
	 */
	private boolean isXSS(String value) {
		if (StringUtils.isBlank(value)) {
			return false;
		}
		
		value = value.toLowerCase();
		if ((value.contains("<") && value.contains(">"))
				|| value.contains("alert")
				|| value.contains("script")) {
			return true;
		}
		return false;
	}
	
}
